//
//  CYVideoPlayer.swift
//  RLVideo
//
//  Created by raniys on 2018/10/26.
//  Copyright © 2018 raniys. All rights reserved.
//

import UIKit
import AVFoundation

@objc public protocol CYVideoPlayerDelegate: class {
    
    /// 画面回调
    func videoPlayer(videoPlayer: CYVideoPlayer, layerDisplay avPlayer: AVPlayer)
    /// Ready to play with total duration
    func videoPlayer(videoPlayer: CYVideoPlayer, readyToPlayWith totalDuration: Double)
    /// 播放结束
    @objc optional func didFinishedPlaying(videoPlayer: CYVideoPlayer)
    /// 播放进度
    @objc optional func videoPlayer(videoPlayer: CYVideoPlayer, playTo duration: Double)
    /// 进度长度
    @objc optional func scrubProgressWidthOf(videoPlayer: CYVideoPlayer) -> CGFloat
    /// 异常监听
    @objc optional func videoPlayer(videoPlayer: CYVideoPlayer, failedToPrepareWith error: Error)
    
}


public class CYVideoPlayer: NSObject {
    
    // MARK: - Types
    
    
    // MARK: - Properties
    
    public weak var delegate: CYVideoPlayerDelegate?
    
    public var url: URL? {
        didSet {
            guard let url = url else { return }
            avAsset = AVURLAsset(url: url)
        }
    }
    
    public var avAsset: AVAsset? {
        didSet {
            guard let asset = avAsset else { return }
            muteOriginalSound = false
            let requestedKeys = ["playable"]
            asset.loadValuesAsynchronously(forKeys: requestedKeys) {
                DispatchQueue.main.async {
                    self.prepareTo(play: asset, with: requestedKeys)
                }
            }
        }
    }
    
    public var muteOriginalSound: Bool = false {
        didSet {
            
        }
    }
    public fileprivate(set) var totalDuration: Double = 0
    public fileprivate(set) var currentDuration: Double = 0
    
    private var player: AVPlayer?
    private var playerItem: AVPlayerItem?
    private var audioMix: AVMutableAudioMix?
    private var timeObserver: Any?
    
    private var seekToZeroBeforePlay = false
    private var restoreAfterScrubbingRate: Float?
    private var isSeeking = false
    
    
    // MARK: - Life cycle
    
    public override init() {
        super.init()
    }
    
    deinit {
        print("CYVideoPlayer \(#function)")
        
        removePlayerTimeObserver()
        
        player?.removeObserver(self, forKeyPath: "rate")
        player?.removeObserver(self, forKeyPath: "currentItem")
        player?.currentItem?.removeObserver(self, forKeyPath: "status")
        player?.pause()
        
        NotificationCenter.default.removeObserver(self)
    }
    
    
    public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        
        guard
            let observerkeyPath = keyPath,
            let observerChange = change else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }
        print("observeValue: \(observerkeyPath)")

        switch observerkeyPath {
        case "status":
            guard let status = observerChange[NSKeyValueChangeKey.newKey] as? Int else { return }
            
            switch status {
            case AVPlayerItem.Status.unknown.rawValue:
                removePlayerTimeObserver()
                syncScrubber()
            case AVPlayerItem.Status.failed.rawValue:
                if let item = object as? AVPlayerItem, let error = item.error {
                    assetFailedToPrepareForPlayback(error)
                }
            case AVPlayerItem.Status.readyToPlay.rawValue:
                
                initScrubberTimer()
                
                let durationTime: CMTime = playerItemDuration()
                var duration: Double = 0
                if durationTime.isValid {
                    duration = CMTimeGetSeconds(durationTime)
                }
                
                totalDuration = duration
                delegate?.videoPlayer(videoPlayer: self, readyToPlayWith: duration)
            default:
                break
            }
            
        case "currentItem":
            guard
                let _ = observerChange[NSKeyValueChangeKey.newKey] as? AVPlayerItem,
                let player = player else {
                return
            }
            delegate?.videoPlayer(videoPlayer: self, layerDisplay: player)
        default:
            break
        }
    }
    
    
    // MARK: - Video Control
    
    public func play() {
        if seekToZeroBeforePlay {
            seekToZeroBeforePlay = false
            player?.seek(to: CMTime.zero, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
        }
        player?.play()
    }
    
    public func pause() {
        player?.pause()
    }
    
    func mute(_ mute: Bool) {
        player?.isMuted = mute
    }
    
    public func isPlaying() -> Bool {
        return isScrubbing() || (player?.rate ?? 0) != 0
    }
    
    public func resetDisplay() {
        seekToZeroBeforePlay = false
        player?.seek(to: CMTime.zero, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
    }
    
    public func seek(to time: Double) {
        let timescale = playerItem?.asset.duration.timescale ?? 1000
        player?.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: timescale), toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero)
    }
    
    
    // MARK: - Movie scrubber control
    
    private func initScrubberTimer() {
        var interval: Double = 0.1
        
        let durationTime: CMTime = playerItemDuration()
        
        guard durationTime.isValid else { return }
        
        let duration = CMTimeGetSeconds(durationTime)
        if duration.isFinite {
            var progressWidth = UIScreen.main.bounds.size.width
            if let width = delegate?.scrubProgressWidthOf?(videoPlayer: self) {
               progressWidth = width
            }
            
            interval = 0.5 * (Double(duration) / Double(progressWidth))
        }
        
        timeObserver = player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(interval, preferredTimescale: Int32(NSEC_PER_SEC)), queue: nil, using: { [weak self](_) in
            self?.syncScrubber()
        })
        
    }
    
    private func syncScrubber() {
        let durationTime: CMTime = playerItemDuration()
        
        guard durationTime.isValid else {
            if player?.status == .readyToPlay {
                delegate?.videoPlayer?(videoPlayer: self, playTo: 0)
            }
            return
        }
        
        let totalDuration = CMTimeGetSeconds(durationTime)
        
        guard totalDuration.isFinite, let player = player else { return }
        
        currentDuration = CMTimeGetSeconds(player.currentTime())
        
        if player.status == .readyToPlay {
            delegate?.videoPlayer?(videoPlayer: self, playTo: currentDuration)
        }
    }
    
    /// The user is dragging the movie controller thumb to scrub through the movie.
    public func beginScrubbing() {
        restoreAfterScrubbingRate = player?.rate
        player?.rate = 0
        removePlayerTimeObserver()
    }
    
    /// Set the player current time to match the scrubber position.
    public func scrub(_ slider: UISlider) {
        
        guard !isSeeking else { return }
        
        isSeeking = true
        
        let durationTime: CMTime = playerItemDuration()
        guard durationTime.isValid else { return }
        
        let duration = CMTimeGetSeconds(durationTime)
        guard duration.isFinite else { return }
        
        let minValue = slider.minimumValue
        let maxValue = slider.maximumValue
        let value = slider.value
        let time = duration * Double(value - minValue) / Double(maxValue - minValue)
        player?.seek(to: CMTimeMakeWithSeconds(time, preferredTimescale: Int32(NSEC_PER_SEC)), completionHandler: { (finished) in
            DispatchQueue.main.async {
                self.isSeeking = false
            }
        })
    }
    
    public func endScrubbing() {
        if let rate = restoreAfterScrubbingRate {
            player?.setRate(rate, time: CMTime.zero, atHostTime: CMTime.zero)
            restoreAfterScrubbingRate = 0
        }
        
        guard timeObserver == nil else { return }
        
        let durationTime: CMTime = playerItemDuration()
        guard durationTime.isValid else { return }
        
        let duration = CMTimeGetSeconds(durationTime)
        guard duration.isFinite else { return }
        
        var progressWidth = UIScreen.main.bounds.size.width
        if let width = delegate?.scrubProgressWidthOf?(videoPlayer: self) {
            progressWidth = width
        }
        
        let tolerance = 0.5 * (Double(duration) / Double(progressWidth))
        timeObserver = player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(tolerance, preferredTimescale: Int32(NSEC_PER_SEC)), queue: nil, using: { [weak self](_) in
            self?.syncScrubber()
        })
    }
    
    public func isScrubbing() -> Bool {
        return (restoreAfterScrubbingRate ?? 0) != 0
    }


    // MARK: - Player prepare
    
    /// Invoked at the completion of the loading of the values for all keys on the asset that we require.
    /// Checks whether loading was successfull and whether the asset is playable.
    /// If so, sets up an AVPlayerItem and an AVPlayer to play the asset.
    ///
    /// - Parameters:
    ///   - asset: avasset for player
    ///   - keys: required keys
    private func prepareTo(play asset: AVAsset, with keys: [String]) {
        
        for key in keys {
            var error: NSError?
            let keyStatus = asset.statusOfValue(forKey: key, error: &error)
            if keyStatus == .failed {
                assetFailedToPrepareForPlayback(error!)
                return
            }
        }
        
        if !asset.isPlayable {
            let error = NSError(domain: "CYVideoPlayer", code: 0, userInfo: [NSLocalizedDescriptionKey: "Item cannot be played", NSLocalizedFailureReasonErrorKey: "The assets tracks were loaded, but could not be made playable."])
            assetFailedToPrepareForPlayback(error)
        }
        
        if let playerItem = playerItem {
            playerItem.removeObserver(self, forKeyPath: "status")
            
            NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)
        }
        
        playerItem = AVPlayerItem(asset: asset)
        playerItem?.audioMix = audioMix
        playerItem?.addObserver(self, forKeyPath: "status", options: [.initial, .new], context: nil)
        
        
        NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)
        
        seekToZeroBeforePlay = false
        
        if player == nil {
            player = AVPlayer(playerItem: playerItem)
            player?.addObserver(self, forKeyPath: "currentItem", options: [.initial, .new], context: nil)
            player?.addObserver(self, forKeyPath: "rate", options: [.initial, .new], context: nil)
        }
        
        if player?.currentItem != playerItem {
            player?.replaceCurrentItem(with: playerItem)
        }
    }
    
    
    // MARK: - Error Handling
    
    private func assetFailedToPrepareForPlayback(_ error: Error)  {
        removePlayerTimeObserver()
        syncScrubber()
        delegate?.videoPlayer?(videoPlayer: self, failedToPrepareWith: error)
    }
    
    
    
    // MARK: - Helper
    
    private func removePlayerTimeObserver() {
        guard let observer = timeObserver else { return }
        player?.removeTimeObserver(observer)
        timeObserver = nil
    }
    
    private func playerItemDuration() -> CMTime {
        guard let item = player?.currentItem else {
            return CMTime.invalid
        }
        if item.status == .readyToPlay {
            return item.duration
        }
        return CMTime.invalid
    }
    
    @objc private func playerItemDidReachEnd(_ notification: Notification) {
        seekToZeroBeforePlay = true
        delegate?.didFinishedPlaying?(videoPlayer: self)
    }
    
    
}



